/*
Copyright © 2024 weidongkl <weidongkx@gmail.com>
*/

package jaccard

type Jaccard struct {
}

func New() *Jaccard {
	return &Jaccard{}
}

func (c *Jaccard) Calculate(s1, s2 string) (float64, error) {
	return JaccardSimilarity(s1, s2), nil
}

func (c *Jaccard) Name() string {
	return "jaccard"
}

// JaccardSimilarity calculates the Jaccard similarity coefficient between two strings s1 and s2.
// The Jaccard similarity measures the size of the intersection divided by the size of the union of two sets.
// It is widely used in information retrieval and text mining to measure the similarity between documents or sets of words.
// Parameters:
//
//	s1: The first string.
//	s2: The second string.
//
// Return value:
//
//	The Jaccard similarity coefficient, as a float32
//
// the maximum similarity is 1.0, and the minimum similarity is 0.0.
func JaccardSimilarity(s1, s2 string) float64 {
	/*
		Jaccard Similarity
		J(A,B) = |A ∩ B| / |A ∪ B|
	*/
	// Initialize variables to store the lengths of s1 and s2,
	// the number of intersecting characters,
	// and the number of characters in the union.
	var (
		lenS1        = len(s1)
		lenS2        = len(s2)
		intersection = 0
		union        = 0
	)
	// If the two strings are identical, return the maximum similarity of 1.
	if s1 == s2 {
		return 1.0
	}
	// If one of the strings is empty, the similarity is 0.
	if lenS1 == 0 || lenS2 == 0 {
		return 0.0
	}

	// Use a map to record the occurrence of each character in s1.
	// Using a map to store unique characters from s1
	// to minimize comparison counts.
	charSet1 := make(map[rune]int)
	for _, ch := range s1 {
		_, ok := charSet1[ch]
		if ok {
			charSet1[ch] += 1
		} else {
			charSet1[ch] = 1
		}
	}
	// Traverse s2, counting the number of intersecting characters and
	// updating the number of characters in the union.
	for _, ch := range s2 {
		_, ok := charSet1[ch]
		if ok {
			intersection++
			// Remove matched characters to avoid double counting.
			if charSet1[ch] > 1 {
				charSet1[ch] -= 1
			} else {
				delete(charSet1, ch)
			}
			union++
		}
	}

	// Count remaining characters in s1 that do not exist in s2.
	union = lenS1 + lenS2 - intersection
	// If the union is empty, the similarity is 0.
	if union == 0 {
		return 0.0
	}

	return float64(intersection) / float64(union)
}
